SPB Git forge
7commits 1branches 0releases
229.0 KBsize
maindefault branch
13 days agolast push
TypeScript 91.8% HTML 3.2% JavaScript 3% SQL 1.4% CSS 0.7%
8.9 KB · 174 lines tsx
Raw Blame History
1"use client";23import { use } from "react";4import Link from "next/link";5import { ExternalLink } from "lucide-react";6import { useApi } from "@/lib/api";7import { fmtDate, fmtDuration, fmtNum, truncate } from "@/lib/format";8import { Empty, Metric, Panel, Skeleton, SurfaceChips, Table, Tag, TypeTag } from "@/components/ui";9import type { EntityRow } from "@/components/EntityTable";1011interface Detail {12  entity: EntityRow & { first_seen: string; last_seen: string; seen_count: number; first_session: string };13  observations: { session_id: string; step: number; ts: string; surfaces: string[]; snapshot: { name?: string; author?: string; metrics?: Record<string, number>; context?: string; provenance?: { surface: string; confidence: number; detail?: string }[] }; goal: string | null }[];14  relations: { rel_type: string; from_fp: string; to_fp: string; from_name: string | null; to_name: string | null; from_type: string; to_type: string; step: number }[];15  media: { duration_s: number | null; width: number | null; height: number | null; delivery: { kind: string; hostnames: string[] } | null; frames: string[] | null; thumbnail_url: string | null } | null;16  feed: { session_id: string; step: number; page_type: string; feed_position: number; visible: boolean; ts: string }[];17}1819export default function EntityPage({ params }: { params: Promise<{ fp: string }> }) {20  const { fp } = use(params);21  const { data, loading, error } = useApi<Detail>(`/entities/${encodeURIComponent(decodeURIComponent(fp))}`);22  if (loading) return <Skeleton rows={10} />;23  if (error || !data) return <Empty>{error ?? "not found"}</Empty>;24  const e = data.entity;25  const type = e.entity_type ?? e.type ?? "?";26  return (27    <div className="space-y-4">28      <div className="text-xs text-dim mono">29        <Link href="/entities" className="hover:text-cyan">entities</Link> / {e.fingerprint}30      </div>31      <div className="flex flex-wrap items-start gap-4">32        <div className="min-w-0">33          <div className="flex items-center gap-2">34            <TypeTag type={type} />35            <Tag>{e.platform}</Tag>36            {e.url && (37              <a href={e.url} target="_blank" rel="noreferrer" className="text-dim hover:text-cyan flex items-center gap-1 text-xs">38                open on platform <ExternalLink className="h-3.5 w-3.5" />39              </a>40            )}41          </div>42          <h1 className="text-xl font-semibold tracking-tight mt-2 max-w-3xl">{e.name ?? e.text_excerpt ?? e.platform_id}</h1>43          {e.author && <div className="text-sm text-fg-2 mt-1">by {e.author}</div>}44          {e.text_excerpt && e.name && <p className="text-sm text-fg-2 mt-2 max-w-3xl">{e.text_excerpt}</p>}45        </div>46      </div>4748      <div className="grid grid-cols-1 xl:grid-cols-3 gap-4">49        <Panel title="Evidence per field" sub="every canonical value points back to the surface that produced it (§38)" className="xl:col-span-2" pad={false}>50          <div className="p-4">51            <Table head={["field", "value", "evidence"]} dense>52              {Object.entries(e.fields ?? {}).map(([k, f]) => (53                <tr key={k}>54                  <td className="mono text-fg-2 whitespace-nowrap">{k}</td>55                  <td className="max-w-[480px] break-words">{typeof f.value === "string" ? f.value : JSON.stringify(f.value)}</td>56                  <td>57                    <SurfaceChips provenance={f.provenance} />58                    {f.provenance?.[0]?.detail && <div className="text-[10px] text-dim mono truncate max-w-[280px]">{f.provenance[0].detail}</div>}59                  </td>60                </tr>61              ))}62            </Table>63          </div>64        </Panel>65        <div className="space-y-4">66          <Panel title="Canonical record">67            <Metric label="platform id" value={e.platform_id ?? "—"} />68            <Metric label="confidence" value={e.confidence ? Number(e.confidence).toFixed(2) : "—"} />69            <Metric label="observed" value={`${e.seen_count}× in ${new Set(data.observations.map((o) => o.session_id)).size} session(s)`} />70            <Metric label="first seen" value={fmtDate(e.first_seen)} />71            <Metric label="last seen" value={fmtDate(e.last_seen)} />72            {e.metrics &&73              Object.entries(e.metrics).map(([k, v]) => (74                <Metric key={k} label={k} value={fmtNum(v)} />75              ))}76            {data.media && (77              <>78                <Metric label="duration" value={fmtDuration(data.media.duration_s)} />79                {data.media.width ? <Metric label="resolution" value={`${data.media.width}×${data.media.height}`} /> : null}80                {data.media.delivery && <Metric label="delivery" value={`${data.media.delivery.kind} · ${data.media.delivery.hostnames.slice(0, 2).join(", ")}`} />}81              </>82            )}83          </Panel>84          {data.media?.frames?.length ? (85            <Panel title="Sampled frames" sub="0 / 25 / 50 / 75 / 100 % (LEVEL 2)">86              <div className="grid grid-cols-5 gap-1">87                {data.media.frames.map((f) => (88                  // eslint-disable-next-line @next/next/no-img-element89                  <img key={f} src={`/media/${f.split("/media/")[1] ?? ""}`} alt="" className="aspect-video object-cover rounded" />90                ))}91              </div>92            </Panel>93          ) : data.media?.thumbnail_url || e.media?.thumbnail_url ? (94            <Panel title="Thumbnail">95              {/* eslint-disable-next-line @next/next/no-img-element */}96              <img src={data.media?.thumbnail_url ?? e.media?.thumbnail_url ?? ""} alt="" className="rounded w-full" />97            </Panel>98          ) : null}99        </div>100      </div>101102      <div className="grid grid-cols-1 xl:grid-cols-2 gap-4">103        <Panel title="Observations" sub="raw sightings, never overwritten (§37)" pad={false}>104          <div className="p-4">105            <Table head={["when", "session", "step", "surfaces", "context", "name as seen"]} dense>106              {data.observations.map((o, i) => (107                <tr key={i}>108                  <td className="mono text-dim whitespace-nowrap">{fmtDate(o.ts)}</td>109                  <td>110                    <Link href={`/sessions/${o.session_id}`} className="mono text-[11px] hover:text-cyan">111                      {truncate(o.goal ?? o.session_id, 32)}112                    </Link>113                  </td>114                  <td className="mono">#{o.step}</td>115                  <td>116                    <SurfaceChips provenance={o.snapshot.provenance ?? o.surfaces.map((s) => ({ surface: s, confidence: 0 }))} />117                  </td>118                  <td className="text-dim text-[11px]">{o.snapshot.context}</td>119                  <td className="text-fg-2 max-w-[260px] truncate">{o.snapshot.name}</td>120                </tr>121              ))}122            </Table>123          </div>124        </Panel>125        <div className="space-y-4">126          <Panel title="Relationships" pad={false}>127            <div className="p-4">128              {data.relations.length ? (129                <Table head={["from", "relation", "to"]} dense>130                  {data.relations.map((r, i) => (131                    <tr key={i}>132                      <td className="max-w-[240px] truncate">133                        <TypeTag type={r.from_type ?? "?"} /> <Link href={`/entities/${encodeURIComponent(r.from_fp)}`} className="hover:text-cyan">{truncate(r.from_name ?? r.from_fp, 40)}</Link>134                      </td>135                      <td>136                        <Tag color="#9d7bff">{r.rel_type}</Tag>137                      </td>138                      <td className="max-w-[240px] truncate">139                        <TypeTag type={r.to_type ?? "?"} /> <Link href={`/entities/${encodeURIComponent(r.to_fp)}`} className="hover:text-cyan">{truncate(r.to_name ?? r.to_fp, 40)}</Link>140                      </td>141                    </tr>142                  ))}143                </Table>144              ) : (145                <Empty>no relationships recorded</Empty>146              )}147            </div>148          </Panel>149          <Panel title="Feed exposure" sub="where it ranked when the crawler saw it (§44)" pad={false}>150            <div className="p-4">151              {data.feed.length ? (152                <Table head={["when", "page", "position", "visible"]} dense>153                  {data.feed.map((f, i) => (154                    <tr key={i}>155                      <td className="mono text-dim whitespace-nowrap">{fmtDate(f.ts)}</td>156                      <td>157                        <Tag color="#38e1ff">{f.page_type}</Tag>158                      </td>159                      <td className="mono">#{f.feed_position + 1}</td>160                      <td className="mono">{f.visible ? "yes" : "below fold"}</td>161                    </tr>162                  ))}163                </Table>164              ) : (165                <Empty>not seen in a feed</Empty>166              )}167            </div>168          </Panel>169        </div>170      </div>171    </div>172  );173}174